/*
 * Copyright 2020 Dash Core Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file was generated by SWIG (http://www.swig.org) and modified.
 * Version 3.0.12
 */
package org.bitcoinj.core;

import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptException;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.bitcoinj.core.Coin.FIFTY_COINS;
import static org.bitcoinj.script.ScriptOpCodes.OP_NOP;
import static org.bitcoinj.script.ScriptOpCodes.OP_TRUE;

class ChainLockRule extends Rule {

    int height;
    Block block;
    Sha256Hash hashChainTipAfterLock;

    ChainLockRule(String ruleName, Block block, int height) {
        super(ruleName);
        this.block = block;
        this.height = height;
    }

    ChainLockRule(String ruleName, Block block, int height, Sha256Hash hashChainTipAfterLock) {
        this(ruleName, block, height);
        this.hashChainTipAfterLock = hashChainTipAfterLock;
    }
}

public class ChainLockBlockTestGenerator extends FullBlockTestGenerator {

    public ChainLockBlockTestGenerator(NetworkParameters params) {
        super(params);
    }
/*
    @Override
    public RuleList getBlocksToTest(boolean runBarelyExpensiveTests, boolean runExpensiveTests, File blockStorageFile) throws ScriptException, ProtocolException, IOException {
        final FileOutputStream outStream = blockStorageFile != null ? new FileOutputStream(blockStorageFile) : null;

        final Script OP_TRUE_SCRIPT = new ScriptBuilder().op(OP_TRUE).build();
        final Script OP_NOP_SCRIPT = new ScriptBuilder().op(OP_NOP).build();

        // TODO: Rename this variable.
        List<Rule> blocks = new LinkedList<Rule>() {
            @Override
            public boolean add(Rule element) {
                if (outStream != null && element instanceof BlockAndValidity) {
                    try {
                        Utils.uint32ToByteStreamBE(params.getPacketMagic(), outStream);
                        byte[] block = ((BlockAndValidity)element).block.bitcoinSerialize();
                        byte[] length = new byte[4];
                        Utils.uint32ToByteArrayBE(block.length, length, 0);
                        outStream.write(Utils.reverseBytes(length));
                        outStream.write(block);
                        ((BlockAndValidity)element).block = null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
                return super.add(element);
            }
        };
        RuleList ret = new RuleList(blocks, hashHeaderMap, 10);

        Queue<TransactionOutPointWithValue> spendableOutputs = new LinkedList<>();

        int chainHeadHeight = 1;
        Block chainHead = params.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
        blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), 1, "Initial Block"));
        spendableOutputs.offer(new TransactionOutPointWithValue(
                new TransactionOutPoint(params, 0, chainHead.getTransactions().get(0).getTxId()),
                FIFTY_COINS, chainHead.getTransactions().get(0).getOutputs().get(0).getScriptPubKey()));
        for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) {
            chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
            chainHeadHeight++;
            blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), i+1, "Initial Block chain output generation"));
            spendableOutputs.offer(new TransactionOutPointWithValue(
                    new TransactionOutPoint(params, 0, chainHead.getTransactions().get(0).getTxId()),
                    FIFTY_COINS, chainHead.getTransactions().get(0).getOutputs().get(0).getScriptPubKey()));
        }

        // Start by building a couple of blocks on top of the genesis block.
        NewBlock b1 = createNextBlock(chainHead, chainHeadHeight + 1, spendableOutputs.poll(), null);
        blocks.add(new BlockAndValidity(b1, true, false, b1.getHash(), chainHeadHeight + 1, "b1"));
        spendableOutputs.offer(b1.getCoinbaseOutput());

        TransactionOutPointWithValue out1 = spendableOutputs.poll(); checkState(out1 != null);
        NewBlock b2 = createNextBlock(b1, chainHeadHeight + 2, out1, null);
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight + 2, "b2"));
        // Make sure nothing funky happens if we try to re-add b2
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight + 2, "b2"));
        spendableOutputs.offer(b2.getCoinbaseOutput());
        // We now have the following chain (which output is spent is in parentheses):
        //     genesis -> b1 (0) -> b2 (1)
        //
        // so fork like this:
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1)
        //
        // Nothing should happen at this point. We saw b2 first so it takes priority.
        NewBlock b3 = createNextBlock(b1, chainHeadHeight + 2, out1, null);
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight + 2, "b3"));
        // Make sure nothing breaks if we add b3 twice
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight + 2, "b3"));

        // Do a simple UTXO query.
        UTXORule utxo1;
        {
            Transaction coinbase = b2.block.getTransactions().get(0);
            TransactionOutPoint outpoint = new TransactionOutPoint(params, 0, coinbase.getTxId());
            long[] heights = {chainHeadHeight + 2};
            UTXOsMessage result = new UTXOsMessage(params, ImmutableList.of(coinbase.getOutput(0)), heights, b2.getHash(), chainHeadHeight + 2);
            utxo1 = new UTXORule("utxo1", outpoint, result);
            blocks.add(utxo1);
        }

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked", b2.block, 7);
            blocks.add(chainLockRule);
        }

        // Now we add another block to make the alternative chain longer.
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1) -> b4 (2)
        //
        TransactionOutPointWithValue out2 = checkNotNull(spendableOutputs.poll());
        NewBlock b4 = createNextBlock(b3, chainHeadHeight + 3, out2, null);
        blocks.add(new BlockAndValidity(b4, true, false, b2.getHash(), chainHeadHeight + 2, "b4"));

        // Set the chainlock on block b3
        {
            ChainLockRule chainLockRule = new ChainLockRule("b3 chainlocked", b3.block, 7);
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked", b2.block, 7);
            blocks.add(chainLockRule);
        }

        // Now we add another block to make the alternative chain longer, but b2 still has the chain lock.
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1) -> b4 (2) -> b4a (3)
        //
        TransactionOutPointWithValue out2a = checkNotNull(spendableOutputs.poll());
        NewBlock b4a = createNextBlock(b4, chainHeadHeight + 4, out2a, null);
        blocks.add(new BlockAndValidity(b4a, true, false, b2.getHash(), chainHeadHeight + 2, "b4"));

        // Set the chainlock on block b3
        //{
        //    ChainLockRule chainLockRule = new ChainLockRule("b3 chainlocked", b3.block, 7);
        //    blocks.add(chainLockRule);
        //}

        // Set the chainlock on block b2
        //{
        //    ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked", b2.block, 7);
        //    blocks.add(chainLockRule);
        //}

        // Set the chainlock on block b4
        //{
        //    ChainLockRule chainLockRule = new ChainLockRule("b4 chainlocked", b4.block, chainHeadHeight + 3);
        //    blocks.add(chainLockRule);
        //}

        // ... and back to the first chain.
        NewBlock b5 = createNextBlock(b2, chainHeadHeight + 3, out2, null);
        blocks.add(new BlockAndValidity(b5, true, false, b5.getHash(), chainHeadHeight + 3, "b5"));
        spendableOutputs.offer(b5.getCoinbaseOutput());

        // Set the chainlock on block b5
        {
            ChainLockRule chainLockRule = new ChainLockRule("b5 chainlocked", b2.block, chainHeadHeight + 3, b5.getHash());
            blocks.add(chainLockRule);
        }

        if (outStream != null)
            outStream.close();

        // (finally) return the created chain
        return ret;
    }
*/
    public RuleList getBlocksToTest(boolean runBarelyExpensiveTests, boolean runExpensiveTests, File blockStorageFile) throws ScriptException, ProtocolException, IOException {
        final FileOutputStream outStream = blockStorageFile != null ? new FileOutputStream(blockStorageFile) : null;

        final Script OP_TRUE_SCRIPT = new ScriptBuilder().op(OP_TRUE).build();
        final Script OP_NOP_SCRIPT = new ScriptBuilder().op(OP_NOP).build();

        // TODO: Rename this variable.
        List<Rule> blocks = new LinkedList<Rule>() {
            @Override
            public boolean add(Rule element) {
                if (outStream != null && element instanceof BlockAndValidity) {
                    try {
                        Utils.uint32ToByteStreamBE(params.getPacketMagic(), outStream);
                        byte[] block = ((BlockAndValidity)element).block.bitcoinSerialize();
                        byte[] length = new byte[4];
                        Utils.uint32ToByteArrayBE(block.length, length, 0);
                        outStream.write(Utils.reverseBytes(length));
                        outStream.write(block);
                        ((BlockAndValidity)element).block = null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
                return super.add(element);
            }
        };
        RuleList ret = new RuleList(blocks, hashHeaderMap, 10);

        Queue<TransactionOutPointWithValue> spendableOutputs = new LinkedList<>();

        int chainHeadHeight = 1;
        Block chainHead = params.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
        blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), 1, "Initial Block"));
        spendableOutputs.offer(new TransactionOutPointWithValue(
                new TransactionOutPoint(params, 0, chainHead.getTransactions().get(0).getTxId()),
                FIFTY_COINS, chainHead.getTransactions().get(0).getOutputs().get(0).getScriptPubKey()));
        for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) {
            chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
            chainHeadHeight++;
            blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), i+1, "Initial Block chain output generation"));
            spendableOutputs.offer(new TransactionOutPointWithValue(
                    new TransactionOutPoint(params, 0, chainHead.getTransactions().get(0).getTxId()),
                    FIFTY_COINS, chainHead.getTransactions().get(0).getOutputs().get(0).getScriptPubKey()));
        }


        for (int i = 0; i < 10; ++i) {
            chainHeadHeight++;
            NewBlock b = createNextBlock(chainHead, chainHeadHeight, spendableOutputs.poll(), null);
            BlockAndValidity blockRule = new BlockAndValidity(b, true, false, b.getHash(), chainHeadHeight, "b" + i);
            blocks.add(blockRule);
            spendableOutputs.offer(b.getCoinbaseOutput());

            ChainLockRule chainLockRule = new ChainLockRule(blockRule.ruleName + " chainlocked",
                    b.block, chainHeadHeight);
            blocks.add(chainLockRule);
            chainHead = b.block;
        }



        // Start by building a couple of blocks on top of the genesis block.
        NewBlock b1 = createNextBlock(chainHead, ++chainHeadHeight, spendableOutputs.poll(), null);
        blocks.add(new BlockAndValidity(b1, true, false, b1.getHash(), chainHeadHeight, "b1"));
        spendableOutputs.offer(b1.getCoinbaseOutput());


        TransactionOutPointWithValue out1 = spendableOutputs.poll(); checkState(out1 != null);
        NewBlock b2 = createNextBlock(b1, ++chainHeadHeight, out1, null);
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight, "b2"));
        // Make sure nothing funky happens if we try to re-add b2
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight, "b2"));
        spendableOutputs.offer(b2.getCoinbaseOutput());

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked", b2.block, chainHeadHeight);
            blocks.add(chainLockRule);
        }

        // We now have the following chain (which output is spent is in parentheses):
        //     genesis -> b1 (0) -> b2 (1)
        //
        // so fork like this:
        //
        //     genesis -> b1 (0) -> b2 (1) *chainlocked
        //                      \-> b3 (1)
        //
        // Nothing should happen at this point. We saw b2 first so it takes priority.
        NewBlock b3 = createNextBlock(b1, chainHeadHeight, out1, null);
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight, "b3"));
        // Make sure nothing breaks if we add b3 twice
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight, "b3"));


        // Now we add another block to make the alternative chain longer.
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1) -> b4 (2)
        //
        TransactionOutPointWithValue out2 = checkNotNull(spendableOutputs.poll());
        NewBlock b4 = createNextBlock(b3, chainHeadHeight +1 , out2, null);
        blocks.add(new BlockAndValidity(b4, true, false, b2.getHash(), chainHeadHeight, "b4"));

        // Set the chainlock on block b4
        {
            ChainLockRule chainLockRule = new ChainLockRule("b4 chainlocked", b4.block, chainHeadHeight + 1);
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked (after block b4)", b2.block, chainHeadHeight);
            blocks.add(chainLockRule);
        }

        // We now have the following chain (which output is spent is in parentheses):
        //     genesis -> b1 (0) -> b2 (1)
        //
        // so fork like this:
        //
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) *chainlocked
        //                      \-> b3 (1) -> b4 (2)
        //
        // ... and back to the first chain.
        NewBlock b5 = createNextBlock(b2, ++chainHeadHeight, out2, null);
        blocks.add(new BlockAndValidity(b5, true, false, b5.getHash(), chainHeadHeight, "b5"));
        spendableOutputs.offer(b5.getCoinbaseOutput());

        // Set the chainlock on block b5
        {
            ChainLockRule chainLockRule = new ChainLockRule("b5 chainlocked", b5.block, chainHeadHeight, b5.getHash());
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked", b2.block, chainHeadHeight, b5.getHash());
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b4
        {
            ChainLockRule chainLockRule = new ChainLockRule("b4 chainlocked", b4.block, chainHeadHeight, b4.getHash());
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b2
        {
            ChainLockRule chainLockRule = new ChainLockRule("b2 chainlocked (with 5 as the tip)", b2.block, chainHeadHeight, b5.getHash());
            blocks.add(chainLockRule);
        }

        // Set the chainlock on block b3
        {
            ChainLockRule chainLockRule = new ChainLockRule("b3 chainlocked", b3.block, chainHeadHeight, b4.getHash());
            blocks.add(chainLockRule);
        }


        if (outStream != null)
            outStream.close();

        // (finally) return the created chain
        return ret;
    }
}
